昨天完成 BaseAgent 後,專案已經有一套共用的模型呼叫流程。今天加入 PM Agent 與 Research Agent,並要求兩個角色回傳固定 JSON,讓 Python 能穩定取得需要的欄位。
PM 負責整理「接下來要做什麼」,Research 則區分「哪些資訊已確認、哪些只是推論」。為了共用結構化輸出的處理方式,我另外建立 StructuredAgent,使用 Pydantic 驗證模型回覆。
StructuredAgent
如果使用者只輸入:
我想做一個可以回答公司產品問題的 Discord Bot。
這句話仍缺少使用對象、資料來源、時程與限制,也可能混著未確認的假設。因此先分成兩個角度:
兩個角色收到同一段需求,但輸出用途不同。
專案固定使用 Pydantic 2.12.5:
python -m pip install "pydantic==2.12.5"
這篇使用 Pydantic 2 的 model_json_schema() 與 model_validate_json()。
PM 的輸出格式如下:
class PMAnalysis(BaseModel):
model_config = ConfigDict(extra="forbid")
goal: NonEmptyText
constraints: list[NonEmptyText]
work_items: list[NonEmptyText]
disagreements: list[NonEmptyText]
goal 是主要目標,constraints 記錄限制,work_items 是可執行工作,disagreements 則保存衝突或分歧。
Research 使用另一份格式:
class ResearchAnalysis(BaseModel):
model_config = ConfigDict(extra="forbid")
known_information: list[NonEmptyText]
reasonable_inferences: list[NonEmptyText]
items_to_verify: list[NonEmptyText]
known_information:使用者已明確提供的資訊reasonable_inferences:可以推論,但還不是事實的內容items_to_verify:需要另外確認的項目這樣可以避免後面的 Agent 把推測一路傳下去,最後看起來像正式需求。
兩個 Model 都使用 ConfigDict(extra="forbid"),多出未定義欄位就會失敗。NonEmptyText 也會移除前後空白並拒絕空字串。
StructuredAgentBaseAgent 已經負責組合 Prompt、呼叫 LLM Service 與處理一般錯誤。StructuredAgent 在取得回答後,多做一次 Pydantic 驗證:
class StructuredAgent(Generic[OutputModel]):
async def respond(self, user_input: str) -> OutputModel:
response = await self._base_agent.respond(user_input)
try:
return self.output_model.model_validate_json(response.content)
except ValidationError as error:
raise AgentError(
f"{self.config.name} 回覆格式不正確,請重新產生。"
) from error
PM 傳入 PMAnalysis,Research 則傳入 ResearchAnalysis。共用流程不需要知道每個角色有哪些欄位。
原始需求
↓
PM Agent 或 Research Agent
↓
StructuredAgent → BaseAgent → LLM Service
↓
JSON 文字
↓
PMAnalysis 或 ResearchAnalysis
Pydantic Model 能直接產生 Schema:
PMAnalysis.model_json_schema()
ResearchAnalysis.model_json_schema()
Schema 會放進 AgentConfig 並傳給 Ollama;Python 收到 JSON 後,再用 model_validate_json() 驗證欄位、型別與內容。
合法 JSON 不一定是可用資料。少了必要欄位、出現空字串或多出未定義欄位,都會在第二道檢查被擋下。
PM 與 Research 都使用:
temperature = 0.2
seed = 42
max_output_tokens = 500
兩者的差異集中在角色、System Prompt 與 JSON Schema。整理需求與分類資訊需要比較穩定的結果,因此先使用低溫度。
PM 的 Prompt 另外要求沒有分歧時回傳空陣列,後續程式便能直接處理 disagreements,不必先判斷欄位是否存在。
如果回覆無法通過 Pydantic 驗證,StructuredAgent 會把 ValidationError 轉成專案自己的 AgentError:
PM Agent 回覆格式不正確,請重新產生。
上層只需處理 AgentError,錯誤訊息也會保留 Agent 名稱。目前尚未加入自動重試。
現階段尚未建立共用 Context,測試直接把同一段需求交給兩個 Agent:
pm_result = await pm_agent.respond(requirement)
research_result = await research_agent.respond(requirement)
tests/test_project_agents.py 使用 FakeProjectLLMService 回傳事先準備的 JSON,不會啟動 Ollama。測試會確認:
PMAnalysis
ResearchAnalysis
AgentError
python -m unittest discover -s tests -v
Ran 35 tests in 0.097s
OK
DAY 12 新增 4 個 PM 與 Research 測試。這只能證明 Python 端的格式與錯誤處理正確,因為測試沒有真的呼叫 Qwen。
DAY 12 完成 PM、Research 與共用的 StructuredAgent。目前兩個角色仍各自分析原始需求,尚未建立共享 Context;下一篇會加入 Creative 與 Finance,讓專案從需求分析往提案和風險評估延伸。